package dk.kapetanovic.jaft.log;


import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

import dk.kapetanovic.jaft.action.Action;
import dk.kapetanovic.jaft.exception.InconsistentStateException;
import dk.kapetanovic.jaft.exception.TransactionException;

public class OutputJournal implements Journal {
	
	public static final int COMMITED = -1;
	public static final int UNDONE = -2;
	public static final int NO_STATE = -3;
	
	private Journal journal;
	
	private RandomAccessFile rw;
	private ArrayList<Long> pointers = new ArrayList<Long>();
	private long lastLegalState = 0;
	private boolean rafClosed = false;
	private File journalFile;

	public OutputJournal(Journal journal, File journalFile) 
			throws FileNotFoundException, IOException {
		this.journal = journal;
		this.journalFile = journalFile;
		if(journalFile.exists())
			throw new IOException("Journal file already exists: " +
				journalFile.getAbsolutePath());
		
		rw = new RandomAccessFile(journalFile, "rw");
	}

	@Override
	public void addAction(Action action) throws TransactionException {
		try {
			markState();
		}
		catch(IOException e1) {
			try {
				closeRaf();
			} catch (IOException e2) {}
			
			throw new TransactionException(e1);			
		}
		
		boolean flag = true;
		try {
			writeAction(action);
			journal.addAction(action);
			flag = false;
		}
		catch(IOException e1) {
			throw new TransactionException(e1);		
		}
		finally {
			if(flag)
				try {
					resetState();
					closeRaf();
				} 
				catch (IOException e2) {
					try {
						closeRaf();
					} catch (IOException e3) {}
				}
		}
	}

	@Override
	public List<Action> commit() throws TransactionException {
		try {
			writeCommit();
			closeRaf();
			return journal.commit();
		}
		catch(IOException e) {
			throw new TransactionException(e);
		}
		finally {
			try {
				closeRaf();
			} catch (IOException e1) {}
		}
	}
	
	@Override
	public void close() throws TransactionException {
		try {
			journal.close();
			closeRaf();
			if(!journalFile.delete())
				throw new TransactionException("Could not delete journal file: " +
					journalFile.getAbsolutePath());
		} 
		catch (IOException e) {
			throw new TransactionException(e);
		}
		finally {
			try {
				closeRaf();
			} catch (IOException e) {}
		}
	}

	@Override
	public int size() {
		return journal.size();
	}
	
	@Override
	public Action undo() throws InconsistentStateException, TransactionException {
		if(size() != pointers.size())
			throw new InconsistentStateException("");
		
		boolean close = true;
		try {
			Action action = journal.undo();
			long ptr = pointers.remove(pointers.size() - 1);
			writeUndo(ptr);
			close = false;
			return action;
		}
		catch (IOException e) {
			throw new TransactionException(e);
		}
		finally {
			if(close)
				try {
					closeRaf();
				} catch (IOException e1) {}
		}
	}

	private void flushRaf() throws IOException {
		rw.getFD().sync();
	}
	
	private void closeRaf() throws IOException {
		if(!rafClosed)
			rw.close();
		
		rafClosed = true;
	}
	
	private void writeUndo(long ptr) throws IOException {
		rw.seek(ptr);
		rw.writeInt(UNDONE);
		flushRaf();
	}
	
	private void writeCommit() throws IOException {
		rw.writeInt(COMMITED);
		flushRaf();
	}
	
	private void markState() throws IOException {
		lastLegalState = rw.getFilePointer();
	}
	
	private void resetState() throws IOException {
		rw.seek(lastLegalState);
		rw.setLength(lastLegalState);
	}
	
	private void writeAction(Action action) throws IOException {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bos);
		oos.writeObject(action);
		oos.flush();
		oos.close();
		bos.close();
		byte[] data = bos.toByteArray();
		
		long ptr = rw.getFilePointer();
		rw.writeInt(NO_STATE);
		rw.writeInt(data.length);
		rw.write(data);
		flushRaf();
		pointers.add(ptr);
	}
}
